// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Squashfs - a compressed read only filesystem for Linux
 *
 * Copyright (c) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009
 * Phillip Lougher <phillip@squashfs.org.uk>
 *
 * decompressor.c
 */

#include <linux/types.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/buffer_head.h>

#include "squashfs_fs.h"
#include "squashfs_fs_sb.h"
#include "decompressor.h"
#include "squashfs.h"
#include "page_actor.h"

/*
 * This file (and decompressor.h) implements a decompressor framework for
 * Squashfs, allowing multiple decompressors to be easily supported
 */
#ifndef CONFIG_RTOS_SQUASHFS_LZMA
static const struct squashfs_decompressor squashfs_lzma_unsupported_comp_ops = {
	NULL, NULL, NULL, NULL, LZMA_COMPRESSION, "lzma", 0
};
#endif

#ifndef CONFIG_SQUASHFS_LZ4
static const struct squashfs_decompressor squashfs_lz4_comp_ops = {
	NULL, NULL, NULL, NULL, LZ4_COMPRESSION, "lz4", 0
};
#endif

#ifndef CONFIG_SQUASHFS_LZO
static const struct squashfs_decompressor squashfs_lzo_comp_ops = {
	NULL, NULL, NULL, NULL, LZO_COMPRESSION, "lzo", 0
};
#endif

#ifndef CONFIG_SQUASHFS_XZ
static const struct squashfs_decompressor squashfs_xz_comp_ops = {
	NULL, NULL, NULL, NULL, XZ_COMPRESSION, "xz", 0
};
#endif

#ifndef CONFIG_SQUASHFS_ZLIB
static const struct squashfs_decompressor squashfs_zlib_comp_ops = {
	NULL, NULL, NULL, NULL, ZLIB_COMPRESSION, "zlib", 0
};
#endif

#ifndef CONFIG_SQUASHFS_ZSTD
static const struct squashfs_decompressor squashfs_zstd_comp_ops = {
	NULL, NULL, NULL, NULL, ZSTD_COMPRESSION, "zstd", 0
};
#endif

static const struct squashfs_decompressor squashfs_unknown_comp_ops = {
	NULL, NULL, NULL, NULL, 0, "unknown", 0
};

static const struct squashfs_decompressor *decompressor[] = {
	&squashfs_zlib_comp_ops,
	&squashfs_lz4_comp_ops,
	&squashfs_lzo_comp_ops,
	&squashfs_xz_comp_ops,
#ifndef CONFIG_RTOS_SQUASHFS_LZMA
	&squashfs_lzma_unsupported_comp_ops,
#else
	&squashfs_lzma_comp_ops,
#endif
	&squashfs_zstd_comp_ops,
	&squashfs_unknown_comp_ops
};


const struct squashfs_decompressor *squashfs_lookup_decompressor(int id)
{
	int i;

	for (i = 0; decompressor[i]->id; i++)
		if (id == decompressor[i]->id)
			break;

	return decompressor[i];
}


static void *get_comp_opts(struct super_block *sb, unsigned short flags)
{
	struct squashfs_sb_info *msblk = sb->s_fs_info;
	void *buffer = NULL, *comp_opts;
	struct squashfs_page_actor *actor = NULL;
	int length = 0;

	/*
	 * Read decompressor specific options from file system if present
	 */
	if (SQUASHFS_COMP_OPTS(flags)) {
		buffer = kmalloc(PAGE_SIZE, GFP_KERNEL);
		if (buffer == NULL) {
			comp_opts = ERR_PTR(-ENOMEM);
			goto out;
		}

		actor = squashfs_page_actor_init(&buffer, 1, 0);
		if (actor == NULL) {
			comp_opts = ERR_PTR(-ENOMEM);
			goto out;
		}

		length = squashfs_read_data(sb,
			sizeof(struct squashfs_super_block), 0, NULL, actor);

		if (length < 0) {
			comp_opts = ERR_PTR(length);
			goto out;
		}
	}

	comp_opts = squashfs_comp_opts(msblk, buffer, length);

out:
	kfree(actor);
	kfree(buffer);
	return comp_opts;
}


void *squashfs_decompressor_setup(struct super_block *sb, unsigned short flags)
{
	struct squashfs_sb_info *msblk = sb->s_fs_info;
	void *stream, *comp_opts = get_comp_opts(sb, flags);

	if (IS_ERR(comp_opts))
		return comp_opts;

	stream = msblk->thread_ops->create(msblk, comp_opts);
	if (IS_ERR(stream))
		kfree(comp_opts);

	return stream;
}

#ifdef CONFIG_RTOS_SQUASHFS_BCJ
static void bcj_arm64(unsigned char *buffer, size_t last, size_t size)
{
	size_t i;

	for (i = 0; i + 4 <= size; i += 4) {
		if (buffer[i + 3] == 0x94 || buffer[i + 3] == 0x97) {
			unsigned int dest;
			unsigned int src = ((unsigned int)(buffer[i + 2]) << 16)
					| ((unsigned int)(buffer[i + 1]) << 8)
					| (unsigned int)(buffer[i + 0]);
			src <<= 2;
			dest = src - (unsigned int)(i + last);
			dest >>= 2;
			buffer[i + 2] = (dest >> 16);
			buffer[i + 1] = (dest >> 8);
			buffer[i + 0] = dest;
		}
	}
}

static void bcj_arm(unsigned char *buffer, size_t last, size_t size)
{
	size_t i;

	for (i = 0; i + 4 <= size; i += 4) {
		if (buffer[i + 3] == 0xEB) {
			unsigned int dest;
			unsigned int src = ((unsigned int)(buffer[i + 2]) << 16)
					| ((unsigned int)(buffer[i + 1]) << 8)
					| (unsigned int)(buffer[i + 0]);
			src <<= 2;
			dest = src - ((unsigned int)(i + last) + 8);
			dest >>= 2;
			buffer[i + 2] = (dest >> 16);
			buffer[i + 1] = (dest >> 8);
			buffer[i + 0] = dest;
		}
	}
}

static inline void bcj_process(unsigned char *buffer, size_t last, size_t size, int bcj_index)
{
	switch (bcj_index) {
	case BCJ_ARM:
		bcj_arm(buffer, last, size);
		break;
	case BCJ_ARM64:
		bcj_arm64(buffer, last, size);
		break;
	default:
		/* Exception check has been done before here. */
		return;
	}
}

void bcj_process_page_actor(struct squashfs_page_actor *actor, int total_bytes, int bcj_index)
{
	void *actor_addr;
	int processed_bytes = 0;

	actor_addr = squashfs_first_page(actor);

	while (processed_bytes < total_bytes) {
		int bytes_to_do = min_t(int, PAGE_SIZE,
					  total_bytes - processed_bytes);

		bcj_process(actor_addr, processed_bytes, bytes_to_do, bcj_index);

		processed_bytes += bytes_to_do;
		actor_addr = squashfs_next_page(actor);
		if (!actor_addr)
			break;
	}
	squashfs_finish_page(actor);
}
#endif
